// TODO: CODE REVIEW & CLEANUP!
//
// LutronHardware.cs
//
// Implements HomeUX.Components.LutronHardware.
//

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Controls Lutron "Home Works Interactive" hardware.
/// </summary>
///
/// <remarks>
/// 
/// General usage pattern:
///   1. Construct an instance of LutronHardware.
///   2. Hook up events.
///   3. Call Open (inherited from SimpleSerialHardware).
///   4. To control the hardware, you can use TransmitCommand inherited from SimpleSerialHardware)
///      to send a raw command to the serial port, but in general it's easier to use higher-level
///      control methods like SetOutputGain and SetCrosspointGain.  These methods construct and
///      send a serial command and, when the reply is received, they parse the hardware's reply and
///      call a callback method you provide, providing the parsed reply object as an argument.
///
/// Philosophy about exceptions:
///   -- Programmer errors can lead to exceptions like ArgumentException.
///   -- Hardware problems (including timeouts) won't lead to exceptions, but rather will cause the
///      following to occur:
///        -- Higher-level control methods (see above) will be called with null in place of the
///           parsed reply object.  This lets you clean up your state.
///        -- CommandFailed, WarningReported events will be fired as appropriate.  This lets you
///           centralize reporting of hardware problems to the user.
/// </remarks>
///
[DebuggerDisplay("LutronHardware {HardwareLabel}")]
public class LutronHardware : SimpleSerialHardware<GenericReply>
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Constants
    //

    /// <summary>
    /// Matches a Lutron address in square brackets.
    /// </summary>
    static readonly Regex BracketedAddressRegex = new Regex(@"^\s*\[(.*)\]\s*$");

    /// <summary>
    /// String sent by Lutron, at the beginning of a line, to acknowledge receipt of a command
    /// sent by this driver over <r>m_serialPort</r>.
    /// </summary>
    const string LutronPrompt = "L232> ";

    const string UnexpectedOutputMessage = "Unexpected output from hardware: {0}";

    public event EventHandler<LutronHardwareCommandFailedEventArgs> CommandFailed;

    public event EventHandler<LutronHardwareNotificationReceivedEventArgs> NotificationReceived;

    public event EventHandler<LutronHardwareWarningsReportedEventArgs> WarningsReported;

    public LutronHardware(WorkQueue workQueue, VirtualSerialPort serialPort, string hardwareLabel) :
        base(workQueue, serialPort, SimpleSerialHardwareMode.Line, hardwareLabel)
    {
        serialPort.NewLine = "\r";
        NotificationReceived += OnNotificationReceived;
    }

    Action<LutronNotification> _checkForReply;

    void OnNotificationReceived(object sender, LutronHardwareNotificationReceivedEventArgs e)
    {
        if (_checkForReply != null)
        {
            foreach (LutronNotification notification in e.Notifications)
            {
                if (_checkForReply == null)
                    break;
                _checkForReply(notification); // calling this method can alter <_checkForReply>
            }
        }
    }

    protected override void EndCommand()
    {
        base.EndCommand();
        _checkForReply = null;
    }

/*
    public static bool AreAddressesEqual(int[] a, int[] b)
    {
        if (a.Length != b.Length)
            return false;
        for (int index = 0; index < a.Length; index++)
        {
            if (a[index] != b[index])
                return false;
        }
        return true;
    }
*/

	// keypadAddress = normalized dot-form address
    public void RequestKeypadLedStates(string keypadAddress, Action<RequestKeypadLedStatesReply> onComplete)
    {
        _checkForReply = delegate(LutronNotification genericNotification)
        {
            var notification = genericNotification as LutronKeypadLedStateChangeNotification;
            if (notification == null)
                return;
            if (keypadAddress != notification.Address)
                return;

            EndCommand();
            onComplete(new RequestKeypadLedStatesReply()
            {
                LedStates = notification.LedStates
            });
        };

        TransmitCommand(String.Format("RKLS,{0}", keypadAddress), delegate(GenericReply innerReply)
        {
            // if the RKLS command failed (e.g. due to timeout), report it as complete but with
            // null status
            if (innerReply == null)
            {
                onComplete(null);
                return;
            }
        });
    }

	public void PressKeypadButton(string buttonAddress, Action<GenericReply> onComplete)
	{
        _checkForReply = delegate(LutronNotification genericNotification)
        {
            var notification = genericNotification as LutronKeypadButtonPressNotification;
            if (notification == null)
                return;
            if (buttonAddress != notification.Address)
                return;

            EndCommand();
            onComplete(GenericReply.Reply);
        };

        string[] parts = buttonAddress.Split('.');
        if (parts.Length != 4)
        {
            throw new ArgumentException(
                String.Format("Expected a 4-part address, got: {0}", buttonAddress),
                "buttonAddress");
        }
		TransmitCommand(String.Format("KBP,{0}.{1}.{2},{3}", parts), onComplete);
	}

    // base class overrides

    protected override void ParseReceivedString(string received, ref string pushBack,
        string currentCommand, Action<GenericReply> onReply)
    {
        // Lutron transmission notes:
		//
		// Lutron notifications:
		//   -- "AT&F1E0F1Q1S0=1M0&C0&D0\r" = modem initialization sequence
		//   -- "DL, <address>, <level>\r" = load (RPM/Vareo) dimmer level
		//   -- "KLS, <address>, <button-states>\r" = keypad LED states
		//   -- "KBP, <address>, <button>\r" = keypad button press
		//   -- "KBH, <address>, <button>\r" = keypad button hold
		//   -- "KBR, <address>, <button>\r" = keypad button release
		//   -- "KBDT, <address>, <button>\r" = keypad button double tap
		//   -- "Timeclock Real Time Event At  1:00\r" (example time)
		//   -- "Timeclock Sunrise Event At  7:22\r" (example time)
		//   -- "Timeclock Sunset Event At 14:45\r" (example time)
		//   -- "L232> " acknowledging the last command, preceding the next notification
        //
		// Example of requesting dimmer level:
        //   -- Driver sends: "RDL,1.5.1.1.4\r" // request dimmer level
        //   -- Lutron sends: "\rDL, [01:05:01:01:04],   0\r\rL232> "
        //
		// Example of requesting keypad LED states:
        //   -- Driver sends: "RKLS,1.4.12\r" // request keypad LED state
        //   -- Lutron sends: "\rKLS, [01:04:12], 000110000000000000000010\r\rL232> "
		//
		// Note that Lutron often sends blank lines.
        //
        // This driver treats "L232>" as the acknowledgement that Lutron received the command.  In
        // fact, if the Lutron command is a notification request (e.g. "RDL" or "RKLS"), we need to
        // be prepared for the notification we're looking for coming back before "L232>".
		// 
		// KBDT example (due to user at Lutron keypad): Within 1 second:
		//   -- KBP, [01:04:02], 24
		//   -- KBR, [01:04:02], 24
		//   -- KBDT, [01:04:02], 24
		//   -- DL, [01:05:01:02:02], 100
		//   -- DL, [01:05:01:02:01], 100
		//   -- KBR, [01:04:02], 24
		//   -- KLS, [01:04:02], 010110000001100000000011
		// 
		// KBH/KBR example (due to user at Lutron keypad):
		// Note that [01:04:15], 1 = Game Room pool lable light button
		// Note that [01:01:00:02:03] = Game Room pool table
		//   -- T+0sec: KBP, [01:04:15],  1
		//   -- T+0sec: KBP, [01:04:15],  1
		//   -- T+0sec: DL, [01:01:00:02:03], 100
		//   -- T+0sec: KBR, [01:04:15],  1
		//   -- T+0sec: KLS, [01:04:14], 010100000000000000000000
		//   -- T+0sec: KLS, [01:04:15], 100010000000000000000010
		//   -- T+2sec: KBP, [01:04:15],  1
		//   -- T+2sec: DL, [01:01:00:02:03],   0
		//   -- T+2sec: KLS, [01:04:14], 000110000000000000000000
		//   -- T+2sec: KLS, [01:04:15], 000000000000000000000010
		//   -- T+2sec: KBH, [01:04:15], 1
		//   -- T+2sec: KLS, [01:04:15], 200000000000000000000010
		//   -- T+4sec: KBR, [01:04:15],  1
		//   -- T+4sec: DL, [01:01:00:02:03],   0
		//   -- T+4sec: KBP, [01:04:15],  1
		//   -- T+4sec: KBH, [01:04:15], 1
		//   -- T+7sec: KBR, [01:04:15],  1
		//   -- T+7sec: DL, [01:01:00:02:03],  40
		//   -- T+7sec: KLS, [01:04:14], 000100000000000000000000
		//   -- T+7sec: KLS, [01:04:15], 200010000000000000000010
		//   -- T+11sec: DL, [01:01:00:02:03],  40
		//   -- T+11sec: KLS, [01:04:15], 000010000000000000000010
		//
		// FV example, due to sending command "FV,50,00:00.5,0,1.5.1.7.3":
        //   -- DL, [01:05:01:07:03], 50
        //   -- KLS, [01:04:02], 000010000000000000000000
        //   -- KLS, [01:04:05], 000010000000000000000000
        //   -- KLS, [01:04:10], 000100000000000000000000
        //   -- KLS, [01:04:11], 000000000000000000000010
        //   -- KLS, [01:04:13], 000010000000000000000000
        //   -- KLS, [01:04:15], 000010000000000000000000
        //   -- KLS, [01:04:19], 000100000000000000000000
        //   -- KLS, [01:04:20], 000110000000000000000000
        //   -- KLS, [01:06:04], 010000000000000
		//

        // keep track of notifications
        LazyList<LutronNotification> notifications = new LazyList<LutronNotification>();

        // if we come across miscellaneous issue(s) to report (e.g. the driver sends text we
        // don't expect), <warnings> will be set to the issue(s) to report, otherwise it
        // will be null
        LazyStringList warnings = new LazyStringList();

		// process the batch of notifications sent by Lutron; note that since we split on '\r' the
		// last line is a partial line (or it's String.Empty which can be treated the same)
        string[] lines = received.Split(new char[] { '\r' });
        //int foundAcks = 0; // number of "L232>" prompts found
		for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++)
		{
			// set <line> to the current line
            string line = lines[lineIndex];

			// ignore blank lines
			if (line.Length == 0)
				continue;

			// if a line contains a Lutron prompt, it's an indication that Lutron has acknowledged
			// the last subrequest we sent -- keep track of that fact, and delete the prompt
			if (line.StartsWith(LutronPrompt))
			{
                //notifications.Add(new LutronCommandAcknowledgementNotification());
                //foundAcks++;
				line = line.Substring(LutronPrompt.Length);
			}

            // if this is the last (partial) line, just push it back
            if (lineIndex == lines.Length - 1)
            {
                pushBack = line;
                break;
            }

            // ignore blank lines
            if (line.Length == 0)
                continue;

            // if the line starts with "AT", assume it's a modem initialization sequence and
            // discard it
            if (line.StartsWith("AT"))
                continue;

            // if the line starts with "Timeclock", assume it's something like "Timeclock Sunset
            // Event" or "Timeclock Real Time Event" and discard it
            if (line.StartsWith("Timeclock"))
                continue;

            // interpret the notification
            try
            {
                string[] parts = line.Split(',');
                switch (parts[0])
                {
                case "DL": // load (RPM/Vareo) dimmer level
                    DimmerLevelNotificationReceived(line, parts, notifications);
                    break;
                case "KLS": // keypad LED states
                    KeypadLedStatesNotificationReceived(line, parts, notifications);
                    break;
                case "KBP": // keypad button press
                    KeypadButtonPressNotificationReceived(line, parts, notifications);
                    break; // currently ignored
                case "KBH": // keypad button hold
                    break; // currently ignored
                case "KBR": // keypad button release
                    continue; // currently ignored
                case "KBDT": // keypad button double tap
                    break; // currently ignored
                default:
                    throw new WarningException("Unknown notification from Lutron hardware (ignored): {0}",
						line);
                }
            }
            catch (WarningException ex)
            {
                warnings.Add(ex.Message);
            }
		}

        //// if we got an "L232>" prompt (acknowledging the last subrequest), set state to indicate
        //// that the subrequest was acknowledged, and transmit the next subrequest
        //if (foundAcks > 0)
        //{
        //    // since we don't transmit a command to the Lutron hardware until the previous
        //    // command has been acknowledged, there should only be one acknowledgement in any
        //    // given notification batch -- but there may be as many as two, something went wrong
        //    // with a previous command
        //    if (foundAcks > 2)
        //        warnings.Add("Internal error");
        //}

        // report notifications, if any
        ReportNotifications(notifications);

        // report warnings, if any
        ReportWarnings(warnings);
    }

    /// <summary>
    /// Called when a "DL" notification is received from the Lutron hardware.
    /// </summary>
    ///
    /// <param name="line">The original notification string.</param>
    ///
    /// <param name="parts">The parts of the notification string, split at commas.</param>
    ///
    /// <param name="notifications">Where to report notifications.</param>
    ///
    void DimmerLevelNotificationReceived(string line, string[] parts, LazyList<LutronNotification> notifications)
    {
        // The notification is of the form:
        //     DL, <address>, <level>
        // Example:
        //     DL, [01:05:01:01:04],   0
        //
        if (parts[0] != "DL")
            throw new WarningException("Internal error");
        if (parts.Length != 3)
        {
            throw new WarningException("Expecting {0} comma-separated parts in notification, got {1} parts",
                3, parts.Length);
        }

        /*
        // set <load> to the LutronLoad specified by the address portion of <parts>
        LutronLoad load = FindLutronLoad(ParseAddress(parts[1], 5, null));

        // update the level of <load> to the load level (0-100 inclusive)
        load.Level = ParseIntLoadLevel(parts[2], null);

        // inform any subrequests waiting on <load> that a notification relating to the load was
        // received
        load.NotificationReceived();

        // add a "<ControlStatus>" subreport to <report>
        load.AddControlStatusToReport(ref report, null);
        */
    }

    /// <summary>
    /// Called when a "KLS" notification is received from the Lutron hardware.
    /// </summary>
    ///
    /// <param name="line">The original notification string.</param>
    ///
    /// <param name="parts">The parts of the notification string, split at commas.</param>
    ///
    ///
    /// <param name="notifications">Where to report notifications.</param>
    ///
    void KeypadLedStatesNotificationReceived(string line, string[] parts, LazyList<LutronNotification> notifications)
    {
        // The notification is of the form:
        //     KLS, <address>, <button-states>
        // Example:
        //     KLS, [01:04:12], 000110000000000000000010
        //
        if (parts[0] != "KLS")
            throw new WarningException("Internal error");
        if (parts.Length != 3)
        {
            throw new WarningException("Expecting {0} comma-separated parts in notification, got {1} parts",
                3, parts.Length);
        }

        string ledStatesString = parts[2].Trim();
        var ledStates = new bool[ledStatesString.Length];
        for (int index = 0; index < ledStates.Length; index++)
        {
            char ledStateChar = ledStatesString[index];
            if ((ledStateChar == '0') || (ledStateChar == '2')/*flashing?*/)
                ledStates[index] = false;
            else
            if (ledStateChar == '1')
                ledStates[index] = true;
            else
            {
                throw new WarningException(
                    "Expecting LED states string containing '0', '1', or '2' characters, got: {0}",
                    ledStatesString);
            }
        }

        notifications.Add(new LutronKeypadLedStateChangeNotification()
        {
            Address = NormalizeAddress(parts[1], 3),
            LedStates = ledStates
        });
    }

    /// <summary>
    /// Called when a "KBP" notification is received from the Lutron hardware.
    /// </summary>
    ///
    /// <param name="line">The original notification string.</param>
    ///
    /// <param name="parts">The parts of the notification string, split at commas.</param>
    ///
    /// <param name="notifications">Where to report notifications.</param>
    ///
    void KeypadButtonPressNotificationReceived(string line, string[] parts, LazyList<LutronNotification> notifications)
    {
        // The notification is of the form:
        //     KBP, <keypad-address>, <button-number>
        // Example:
        //     KLS, [01:04:12], 2
        //
        if (parts[0] != "KBP")
            throw new WarningException("Internal error");
        if (parts.Length != 3)
        {
            throw new WarningException("Expecting {0} comma-separated parts in notification, got {1} parts",
                3, parts.Length);
        }

        int[] keypadAddress = ParseAddress(parts[1], 3);
        int buttonId;
        if (!TryParseInt(parts[2], out buttonId))
            throw new WarningException("Invalid Lutron keypad button ID \"{0}\"", parts[1]);

        notifications.Add(new LutronKeypadButtonPressNotification()
        {
            Address = String.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}.{3}",
                keypadAddress[0], keypadAddress[1], keypadAddress[2], buttonId)
        });
    }

    public void ReportCommandFailed(string command)
    {
        if (CommandFailed != null)
        {
            CommandFailed(this, new LutronHardwareCommandFailedEventArgs()
            {
                Command = command
            });
        }
    }

	public void ReportNotifications(LazyList<LutronNotification> notifications)
	{
        if ((notifications.Count > 0) && (NotificationReceived != null))
        {
            NotificationReceived(this, new LutronHardwareNotificationReceivedEventArgs()
            {
                Notifications = notifications.ListOrNull
            });
        }
	}

	public void ReportWarnings(LazyStringList warnings)
	{
        if ((warnings.Count > 0) && (WarningsReported != null))
        {
            WarningsReported(this, new LutronHardwareWarningsReportedEventArgs()
            {
                Messages = warnings.ListOrNull
            });
        }
	}

    /// <summary>
    /// Parses a Lutron hardware element address, either bracketed (e.g. "[1.2.3]") or not
    /// (e.g. "1.2.3"), separated by periods or by colons (e.g. "1:2:3") or both, with a specified
    /// number of numeric parts in the address.
    /// </summary>
    ///
    /// <param name="addressString">The string to parse.</param>
    ///
    /// <param name="partCount">The number of numeric parts expected.</param>
    ///
    /// <returns>
    /// The array of <pr>partCount</pr> parsed numeric parts.
    /// </returns>
    ///
    /// <exception cref="WarningException">
    /// The address format is incorrect.  See <pr>element</pr>.
    /// </exception>
    ///
    public static int[] ParseAddress(string addressString, int partCount)
    {
        // if address is in square brackets, remove the brackets
        Match match = BracketedAddressRegex.Match(addressString);
        if (match.Success)
            addressString = match.Groups[1].Value;

        // parse the address
        string[] parts = addressString.Split('.', ':');
        int[] address = new int[parts.Length];

        int index = 0;
        foreach (string part in parts)
        {
            int addressPart;
            if (!TryParseInt(part, out addressPart))
                    throw new WarningException("Invalid Lutron address: {0}", addressString);
            address[index++] = addressPart;
        }
        if (address.Length != partCount)
        {
            throw new WarningException("Expecting a {0}-part Lutron address, got: {1}",
                partCount, addressString);
        }
        return address;
    }

    static bool TryParseInt(string stringValue, out int result)
    {
        try
        {
            result = XmlConvert.ToInt32(stringValue.Trim());
            return true;
        }
        catch (Exception ex)
        {
            if ((ex is FormatException) || (ex is OverflowException))
            {
                result = 0;
                return false;
            }
            else
                throw;
        }
    }

    // TODO: document
    public static string NormalizeAddress(string addressString, int partCount)
    {
        return FormatAddress(ParseAddress(addressString, partCount));
    }

    // TODO: document
    public static string FormatAddress(int[] address)
    {
        StringBuilder normalizedAddress = new StringBuilder(100);
        foreach (int part in address)
        {
            if (normalizedAddress.Length > 0)
                normalizedAddress.Append('.');
            normalizedAddress.AppendFormat(CultureInfo.InvariantCulture, "{0}", part);
        }
        return normalizedAddress.ToString();
    }

    // nested types

    class WarningException : Exception
    {
        internal WarningException(string format, params object[] args) :
            base(Util.Format(format, args))
	    {
	    }
    }
}

/// <summary>
/// Holds the result of <r>LutronHardware.RequestKeypadLedStates</r>.
/// </summary>
public class RequestKeypadLedStatesReply
{
	/// <summary>
	/// Element <n>i</n> is <n>true</n> if the LED with 1-based ID <n>i + 1</n> is on, <n>false</n>
	/// if it's off.
	/// </summary>
	public bool[] LedStates { get; set; }
}

[DebuggerDisplay("Command = {Command}")]
public class LutronHardwareCommandFailedEventArgs : EventArgs
{
    public string Command { get; internal set; }
}

[DebuggerDisplay("Count = {Notifications.Count}")]
public class LutronHardwareNotificationReceivedEventArgs : EventArgs
{
    public ICollection<LutronNotification> Notifications { get; internal set; }
}

[DebuggerDisplay("Count = {Messages.Count}")]
public class LutronHardwareWarningsReportedEventArgs : EventArgs
{
    public ICollection<string> Messages { get; internal set; }
}

public abstract class LutronNotification
{
    public LutronNotificationKind Kind { get; protected set; }
}

//public class LutronCommandAcknowledgementNotification : LutronNotification
//{
//    internal LutronCommandAcknowledgementNotification()
//    {
//        Kind = LutronNotificationKind.CommandAcknowledgement;
//    }
//}

public class LutronDimmerLevelChangeNotification : LutronNotification
{
    public string Address { get; internal set; } // normalized dot-form 5-part address
    public int Level { get; internal set; } // 0 to 100 inclusive

	internal LutronDimmerLevelChangeNotification()
	{
		Kind = LutronNotificationKind.DimmerLevelChange;
	}
}

public class LutronGrafikEyeSceneChangeNotification : LutronNotification
{
    public string Address { get; internal set; } // normalized dot-form 3- or 4-part address
    public int Scene { get; internal set; } // 0 to 16 inclusive

	internal LutronGrafikEyeSceneChangeNotification()
	{
		Kind = LutronNotificationKind.GrafikEyeSceneChange;
	}
}

public class LutronKeypadButtonDoubleTapNotification : LutronNotification
{
    public string Address { get; internal set; } // normalized dot-form 4-part address including button number

	internal LutronKeypadButtonDoubleTapNotification()
	{
		Kind = LutronNotificationKind.KeypadButtonDoubleTap;
	}
}

public class LutronKeypadButtonHoldNotification : LutronNotification
{
    public string Address { get; internal set; } // normalized dot-form 4-part address including button number

	internal LutronKeypadButtonHoldNotification()
	{
		Kind = LutronNotificationKind.KeypadButtonHold;
	}
}

public class LutronKeypadButtonPressNotification : LutronNotification
{
    public string Address { get; internal set; } // normalized dot-form 4-part address including button number

	internal LutronKeypadButtonPressNotification()
	{
		Kind = LutronNotificationKind.KeypadButtonPress;
	}
}

public class LutronKeypadLedStateChangeNotification : LutronNotification
{
    public string Address { get; internal set; } // normalized dot-form 3-part keypad address

    /// <summary>
    /// Element <n>i</n> is <n>true</n> if the LED with 1-based ID <n>i + 1</n> is on, <n>false</n>
    /// if it's off.
    /// </summary>
    public bool[] LedStates { get; set; }

	internal LutronKeypadLedStateChangeNotification()
	{
        Kind = LutronNotificationKind.KeypadLedStateChange;
	}
}

public enum LutronNotificationKind
{
    CommandAcknowledgement,
	DimmerLevelChange,
	GrafikEyeSceneChange,
	KeypadButtonDoubleTap,
	KeypadButtonHold,
	KeypadButtonPress,
	KeypadLedStateChange
}

}

